/*
 * OREKIT-X
 * Copyright 2002-2008 CS Communication & Systemes
 * 
 * Parts of this software package have been licensed to CS
 * Communication & Systemes (CS) under one or more contributor license
 * agreements.  See the NOTICE file distributed with this work for
 * additional information.
 *  
 * This is an experimental copy of OREKIT from www.orekit.org.
 * Please use the original OREKIT from orekit.org for normal work
 * unrelated to this research project.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you
 * may not use this file except in compliance with the License.  You
 * may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package ore.tle;

import ore.errors.OrekitException;
import ore.time.AbsoluteDate;
import ore.time.DateComponents;
import ore.time.TimeComponents;
import ore.time.TimeScale;
import ore.time.TimeScalesFactory;
import ore.time.TimeStamped;

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
import java.util.regex.Pattern;

/** 
 * A single set of TLE data.
 *
 * TLE sets can be built either by providing directly the two lines,
 * in which case parsing is performed internally or by providing the
 * already parsed elements.
 * 
 * TLE are not transparently convertible to {@link ore.orbits.Orbit
 * Orbit} instances. They are significant only with respect to their
 * dedicated {@link TLEPropagator propagator}, which also computes
 * position and velocity coordinates.  Any attempt to directly use
 * orbital parameters like {@link #getE() eccentricity}, {@link
 * #getI() inclination}, etc. without any reference to the {@link
 * TLEPropagator TLE propagator} is prone to errors.
 * 
 * More information on the TLE format can be found on the <a
 * href="http://www.celestrak.com/">CelesTrak website.</a>
 * 
 * @author Fabien Maussion
 * @author Luc Maisonobe
 */
public class TLE 
    extends Object
    implements TimeStamped
{
    /** 
     * Check TLE line elements for valid format.
     * 
     * @param line1 The first element (69 char String)
     * @param line2 The second element (69 char String)
     * 
     * @return True if format is recognised, false if not
     * 
     * @exception OrekitException Checksum not valid with detail message.
     */
    public static boolean IsFormatOK(final String line1, final String line2)
        throws OrekitException
    {
        if (line1 == null || line1.length() != 69 ||
            line2 == null || line2.length() != 69)
        {
            return false;
        }
        else if (!(LINE_1_PATTERN.matcher(line1).matches() &&
                   LINE_2_PATTERN.matcher(line2).matches()))
        {
            return false;
        }
        else {
            /*
             * Check sums
             */
            final int checksum1 = Checksum(line1);
            if (Integer.parseInt(line1.substring(68)) != (checksum1 % 10)) {
                throw new OrekitException(CHECKSUM_MESSAGE,
                                          1, line1.substring(68) ,
                                          checksum1 % 10, line1);
            }
            else {
                final int checksum2 = Checksum(line2);
                if (Integer.parseInt(line2.substring(68)) != (checksum2 % 10)) {
                    throw new OrekitException(CHECKSUM_MESSAGE,
                                              2, line2.substring(68) ,
                                              checksum2 % 10, line2);
                }
                else
                    return true;
            }
        }
    }
    /** 
     * Identifier for default type of ephemeris (SGP4/SDP4).
     */
    public static final int DEFAULT = 0;
    /** 
     * Identifier for SGP type of ephemeris.
     */
    public static final int SGP = 1;
    /** 
     * Identifier for SGP4 type of ephemeris.
     */
    public static final int SGP4 = 2;
    /** 
     * Identifier for SDP4 type of ephemeris.
     */
    public static final int SDP4 = 3;
    /** 
     * Identifier for SGP8 type of ephemeris.
     */
    public static final int SGP8 = 4;
    /** 
     * Identifier for SDP8 type of ephemeris.
     */
    public static final int SDP8 = 5;
    /** 
     * Pattern for line 1.
     */
    private static final Pattern LINE_1_PATTERN =
        Pattern.compile("1 [ 0-9]{5}U [ 0-9]{5}[ A-Z]{3} [ 0-9]{5}[.][ 0-9]{8} [ +-][.][ 0-9]{8} " +
                        "[ +-][ 0-9]{5}[+-][ 0-9] [ +-][ 0-9]{5}[+-][ 0-9] [ 0-9] [ 0-9]{4}[ 0-9]");
    /** 
     * Pattern for line 2.
     */
    private static final Pattern LINE_2_PATTERN =
        Pattern.compile("2 [ 0-9]{5} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{7} " +
                        "[ 0-9]{3}[.][ 0-9]{4} [ 0-9]{3}[.][ 0-9]{4} [ 0-9]{2}[.][ 0-9]{13}[ 0-9]");
    /** 
     * Checksum error message.
     */
    private static final String CHECKSUM_MESSAGE =
        "wrong checksum of TLE line {0}, expected {1} but got {2} ({3})";
    /** 
     * International symbols for parsing.
     */
    private static final DecimalFormatSymbols SYMBOLS =
        new DecimalFormatSymbols(Locale.US);


    /** 
     * The satellite number.
     */
    public final int satelliteNumber;
    /** 
     * Classification (U for unclassified).
     */
    public final char classification;
    /** 
     * Launch year.
     */
    public final int launchYear;
    /** 
     * Launch number.
     */
    public final int launchNumber;
    /** 
     * Piece of launch (from "A" to "ZZZ").
     */
    public final String launchPiece;
    /** 
     * Type of ephemeris.
     */
    public final int ephemerisType;
    /** 
     * Element number.
     */
    public final int elementNumber;
    /** 
     * The TLE current date.
     */
    public final AbsoluteDate epoch;
    /** 
     * Mean motion (rad/s).
     */
    public final double meanMotion;
    /** 
     * Mean motion first derivative (rad/s<sup>2</sup>).
     */
    public final double meanMotionFirstDerivative;
    /** 
     * Mean motion second derivative (rad/s<sup>3</sup>).
     */
    public final double meanMotionSecondDerivative;
    /** 
     * Eccentricity.
     */
    public final double eccentricity;
    /** 
     * Inclination (rad).
     */
    public final double inclination;
    /** 
     * Argument of perigee (rad).
     */
    public final double pa;
    /** 
     * Right Ascension of the Ascending node (rad).
     */
    public final double raan;
    /** 
     * Mean anomaly (rad).
     */
    public final double meanAnomaly;
    /** 
     * Revolution number at epoch.
     */
    public final int revolutionNumberAtEpoch;
    /** 
     * Ballistic coefficient.
     */
    public final double bStar;
    /** 
     * First line.
     */
    private volatile String line1;
    /** 
     * Second line.
     */
    private volatile String line2;


    /** 
     * Two unparsed lines.  This constructor assumes {@link
     * #IsFormatOK(String, String)} (the format of these lines is OK).
     * A bad format will produce number format or array index out of
     * bounds exceptions.
     * 
     * @param line1 The first element (69 char String)
     * @param line2 The second element (69 char String)
     * 
     * @exception OrekitException if some format error occurs
     */
    public TLE(final String line1, final String line2) throws OrekitException {
        super();
        this.line1 = line1;
        this.line2 = line2;
        /*
         * Identification
         */
        this.satelliteNumber = ParseInteger(line1, 2, 5);
        this.classification  = line1.charAt(7);
        this.launchYear      = ParseYear(line1, 9);
        this.launchNumber    = ParseInteger(line1, 11, 3);
        this.launchPiece     = line1.substring(14, 17).trim();
        this.ephemerisType   = ParseInteger(line1, 62, 1);
        this.elementNumber   = ParseInteger(line1, 64, 4);
        /*
         * Date format transform:
         */
        final DateComponents date = new DateComponents(ParseYear(line1, 18), 1, 1);
        final double dayNb = ParseDouble(line1, 20, 12);
        this.epoch = new AbsoluteDate(new AbsoluteDate(date, TimeComponents.H00,
                                                       TimeScalesFactory.getUTC()),
                                      (dayNb - 1) * 86400); //-1 is due to TLE date definition
        /*
         * Mean motion development
         * converted from rev/day, 2 * rev/day^2 and 6 * rev/day^3 to rad/s, rad/s^2 and rad/s^3
         */
        this.meanMotion                 = ParseDouble(line2, 52, 11) * Math.PI / 43200.0;
        this.meanMotionFirstDerivative  = ParseDouble(line1, 33, 10) * Math.PI / 1.86624e9;
        this.meanMotionSecondDerivative = (Double.parseDouble((line1.substring(44, 45) + '.' +
                                                              line1.substring(45, 50) + 'e' +
                                                              line1.substring(50, 52)).replace(' ', '0')) *
                                           Math.PI / 5.3747712e13);

        this.eccentricity = Double.parseDouble("." + line2.substring(26, 33).replace(' ', '0'));
        this.inclination  = Math.toRadians(ParseDouble(line2, 8, 8));
        this.pa           = Math.toRadians(ParseDouble(line2, 34, 8));
        this.raan         = Math.toRadians(Double.parseDouble(line2.substring(17, 25).replace(' ', '0')));
        this.meanAnomaly  = Math.toRadians(ParseDouble(line2, 43, 8));

        this.revolutionNumberAtEpoch = ParseInteger(line2, 63, 5);
        this.bStar = Double.parseDouble((line1.substring(53, 54) + '.' +
                                         line1.substring(54, 59) + 'e' +
                                         line1.substring(59, 61)).replace(' ', '0'));
    }
    /** 
     * Parsed elements.
     * @param satelliteNumber satellite number
     * @param classification classification (U for unclassified)
     * @param launchYear launch year (all digits)
     * @param launchNumber launch number
     * @param launchPiece launch piece
     * @param ephemerisType type of ephemeris
     * @param elementNumber element number
     * @param epoch elements epoch
     * @param meanMotion mean motion (rad/s)
     * @param meanMotionFirstDerivative mean motion first derivative (rad/s<sup>2</sup>)
     * @param meanMotionSecondDerivative mean motion second derivative (rad/s<sup>3</sup>)
     * @param e eccentricity
     * @param i inclination (rad)
     * @param pa argument of perigee (rad)
     * @param raan right ascension of ascending node (rad)
     * @param meanAnomaly mean anomaly (rad)
     * @param revolutionNumberAtEpoch revolution number at epoch
     * @param bStar ballistic coefficient
     */
    public TLE(final int satelliteNumber, final char classification,
               final int launchYear, final int launchNumber, final String launchPiece,
               final int ephemerisType, final int elementNumber, final AbsoluteDate epoch,
               final double meanMotion, final double meanMotionFirstDerivative,
               final double meanMotionSecondDerivative, final double e, final double i,
               final double pa, final double raan, final double meanAnomaly,
               final int revolutionNumberAtEpoch, final double bStar)
    {
        super();
        /*
         * Identification
         */
        this.satelliteNumber = satelliteNumber;
        this.classification  = classification;
        this.launchYear      = launchYear;
        this.launchNumber    = launchNumber;
        this.launchPiece     = launchPiece;
        this.ephemerisType   = ephemerisType;
        this.elementNumber   = elementNumber;
        /*
         * Orbital parameters
         */
        this.epoch                      = epoch;
        this.meanMotion                 = meanMotion;
        this.meanMotionFirstDerivative  = meanMotionFirstDerivative;
        this.meanMotionSecondDerivative = meanMotionSecondDerivative;
        this.inclination                = i;
        this.raan                       = raan;
        this.eccentricity               = e;
        this.pa                         = pa;
        this.meanAnomaly                = meanAnomaly;

        this.revolutionNumberAtEpoch = revolutionNumberAtEpoch;
        this.bStar                   = bStar;
    }

    /** 
     * @return The satellite number
     */
    public int getSatelliteNumber() {
        return this.satelliteNumber;
    }
    /** 
     * @return Classification
     */
    public char getClassification() {
        return this.classification;
    }
    /** 
     * @return The launch year
     */
    public int getLaunchYear() {
        return this.launchYear;
    }
    /** 
     * @return The launch number
     */
    public int getLaunchNumber() {
        return this.launchNumber;
    }
    /** 
     * @return The launch piece
     */
    public String getLaunchPiece() {
        return this.launchPiece;
    }
    /** 
     * @return The ephemeris type (one of {@link #DEFAULT}, {@link #SGP},
     * {@link #SGP4}, {@link #SGP8}, {@link #SDP4}, {@link #SDP8})
     */
    public int getEphemerisType() {
        return this.ephemerisType;
    }
    /** 
     * @return The element number
     */
    public int getElementNumber() {
        return this.elementNumber;
    }
    /** 
     * Get the TLE current date.
     * @return The epoch
     */
    public AbsoluteDate getDate() {
        return this.epoch;
    }
    /** 
     * @return Mean motion (rad/s)
     */
    public double getMeanMotion() {
        return this.meanMotion;
    }
    /** 
     * @return Mean motion first derivative (rad/s<sup>2</sup>)
     */
    public double getMeanMotionFirstDerivative() {
        return this.meanMotionFirstDerivative;
    }
    /** 
     * @return Mean motion second derivative (rad/s<sup>3</sup>)
     */
    public double getMeanMotionSecondDerivative() {
        return this.meanMotionSecondDerivative;
    }
    /** 
     * @return Eccentricity
     */
    public double getE() {
        return this.eccentricity;
    }
    /** 
     * @return Inclination (rad)
     */
    public double getI() {
        return this.inclination;
    }
    /** 
     * Get the argument of perigee.
     * @return Omega (rad)
     */
    public double getPerigeeArgument() {
        return this.pa;
    }
    /** 
     * Get Right Ascension of the Ascending node.
     * @return RAAN (rad)
     */
    public double getRaan() {
        return this.raan;
    }
    /** 
     * @return Mean anomaly (rad)
     */
    public double getMeanAnomaly() {
        return this.meanAnomaly;
    }
    /** 
     * @return Revolution number at epoch
     */
    public int getRevolutionNumberAtEpoch() {
        return this.revolutionNumberAtEpoch;
    }
    /** 
     * @return Ballistic coefficient, bStar
     */
    public double getBStar() {
        return this.bStar;
    }
    /** 
     * @return First line
     * @exception OrekitException UTC conversion cannot be done
     */
    public String getLine1()
        throws OrekitException
    {
        String line1 = this.line1;
        if (null == line1) {
            final StringBuffer buffer = new StringBuffer();
            final DecimalFormat f38  = new DecimalFormat("##0.00000000", SYMBOLS);
            final DecimalFormat fExp = new DecimalFormat(".00000E0", SYMBOLS);

            buffer.append('1');

            buffer.append(' ');
            buffer.append(AddPadding(satelliteNumber, '0', 5, true));
            buffer.append(classification);

            buffer.append(' ');
            buffer.append(AddPadding(launchYear % 100, '0', 2, true));
            buffer.append(AddPadding(launchNumber, '0', 3, true));
            buffer.append(AddPadding(launchPiece, ' ', 3, false));

            buffer.append(' ');
            final TimeScale utc = TimeScalesFactory.getUTC();
            final int year = epoch.getComponents(utc).getDate().getYear();
            buffer.append(AddPadding(year % 100, '0', 2, true));
            final double day = 1.0 + epoch.durationFrom(new AbsoluteDate(year, 1, 1, utc)) / 86400;
            buffer.append(f38.format(day));

            buffer.append(' ');
            final double n1 = meanMotionFirstDerivative * 1.86624e9 / Math.PI;
            final String sn1 = AddPadding(new DecimalFormat(".00000000", SYMBOLS).format(n1), ' ', 10, true);
            buffer.append(sn1);

            buffer.append(' ');
            final double n2 = meanMotionSecondDerivative * 5.3747712e13 / Math.PI;
            final String doubleDash = "--";
            final String sn2 = fExp.format(n2).replace('E', '-').replace(doubleDash, "-").replace(".", "");
            buffer.append(AddPadding(sn2, ' ', 8, true));

            buffer.append(' ');
            final String sB = fExp.format(bStar).replace('E', '-').replace(doubleDash, "-").replace(".", "");
            buffer.append(AddPadding(sB, ' ', 8, true));

            buffer.append(' ');
            buffer.append(ephemerisType);

            buffer.append(' ');
            buffer.append(AddPadding(elementNumber, ' ', 4, true));

            buffer.append(Integer.toString(Checksum(buffer)));

            line1 = buffer.toString();
            this.line1 = line1;
        }
        return line1;
    }
    /** 
     * @return Second line
     */
    public String getLine2() {
        String line2 = this.line2;
        if (null == line2) {
            final StringBuffer buffer = new StringBuffer();
            final DecimalFormat f34   = new DecimalFormat("##0.0000", SYMBOLS);
            final DecimalFormat f211  = new DecimalFormat("#0.00000000", SYMBOLS);

            buffer.append('2');

            buffer.append(' ');
            buffer.append(AddPadding(satelliteNumber, '0', 5, true));

            buffer.append(' ');
            buffer.append(AddPadding(f34.format(Math.toDegrees(inclination)), ' ', 8, true));
            buffer.append(' ');
            buffer.append(AddPadding(f34.format(Math.toDegrees(raan)), ' ', 8, true));
            buffer.append(' ');
            buffer.append(AddPadding((int) Math.rint(eccentricity * 1.0e7), '0', 7, true));
            buffer.append(' ');
            buffer.append(AddPadding(f34.format(Math.toDegrees(pa)), ' ', 8, true));
            buffer.append(' ');
            buffer.append(AddPadding(f34.format(Math.toDegrees(meanAnomaly)), ' ', 8, true));

            buffer.append(' ');
            buffer.append(AddPadding(f211.format(meanMotion * 43200.0 / Math.PI), ' ', 11, true));
            buffer.append(AddPadding(revolutionNumberAtEpoch, ' ', 5, true));

            buffer.append(Integer.toString(Checksum(buffer)));

            line2 = buffer.toString();
            this.line2 = line2;
        }
        return line2;
    }
    public final int compareTo(TimeStamped stamped){
        return this.getDate().compareTo(stamped);
    }
    /** 
     * The representation is simply the two lines separated by the
     * platform line separator.
     * 
     * @return String representation of this TLE set
     */
    public String toString() {
        try {
            return this.getLine1() + System.getProperty("line.separator") + this.getLine2();
        }
        catch (OrekitException oe) {
            return "???";
        }
    }
    /** 
     * Add padding characters before an integer.
     * 
     * @param k Integer to pad
     * @param c Padding character
     * @param size Desired size
     * @param rightJustified If true, the resulting string is
     * right justified (i.e. space are added to the left)
     * 
     * @return Padded string
     */
    private static String AddPadding(int k, char c, int size, boolean rightJustified)
    {
        return AddPadding(Integer.toString(k), c, size, rightJustified);
    }
    /** 
     * Add padding characters to a string.
     * 
     * @param string String to pad
     * @param c Padding character
     * @param size Desired size
     * @param rightJustified If true, the resulting string is right
     * justified (i.e. space are added to the left)
     * 
     * @return Padded string
     */
    private static String AddPadding(String string, char c, int size, boolean rightJustified)
    {
        final StringBuffer padding = new StringBuffer();
        for (int i = 0; i < size; ++i) {
            padding.append(c);
        }
        if (rightJustified) {
            final String concatenated = padding + string;
            final int l = concatenated.length();
            return concatenated.substring(l - size, l);
        }
        else
            return (string + padding).substring(0, size);
    }
    /** 
     * @param line Line to parse
     * @param start Start index of the first character
     * @param length Length of the string
     * 
     * @return Value of the double
     */
    private static double ParseDouble(final String line, final int start, final int length) {
        return Double.parseDouble(line.substring(start, start + length).replace(' ', '0'));
    }
    /** 
     * @param line Line to parse
     * @param start Start index of the first character
     * @param length Length of the string
     * 
     * @return Value of the integer
     */
    private static int ParseInteger(final String line, final int start, final int length) {
        return Integer.parseInt(line.substring(start, start + length).replace(' ', '0'));
    }
    /** 
     * Parse a year written on 2 digits.
     * 
     * @param line Line to parse
     * @param start Start index of the first character
     * 
     * @return Value of the year
     */
    private static int ParseYear(final String line, final int start) {
        final int year = (2000 + ParseInteger(line, start, 2));
        if (year > 2056) 
            return (year - 100);
        else
            return year;
    }
    /** 
     * Compute the checksum of the first 68 characters of a line.
     * 
     * @param Line line to check
     * 
     * @return Checksum
     */
    private static int Checksum(final CharSequence line) {
        int sum = 0;
        for (int j = 0; j < 68; j++) {
            final char c = line.charAt(j);
            if (Character.isDigit(c)) {
                sum += Character.digit(c, 10);
            } else if (c == '-') {
                ++sum;
            }
        }
        return sum % 10;
    }
}
